﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.U2D;
using System;
using UnityEngine.Networking;
using System.IO;
using System.Text;

[XLua.Hotfix]
public class ResourceManager : MonoSingleton<ResourceManager>
{
    private Dictionary<string, AssetBundle> bundles = new Dictionary<string, AssetBundle>();
    private Dictionary<string, bool> bundleCaches = new Dictionary<string, bool>();
    private static string resUrl = "http://localhost:8080/mmorpg";
    private string localPath = "";
    private string streamAssetPath = "";
    private string localUpdPath = "";
    private string streamAssetAssetsPath = "";
    private string serverAssetsUrl = "http://localhost:8080/mmorpg/Assets";
    private AssetBundleManifest assetBundleManifest = null;  // assetBundle依赖关系
    private string manifest = "manifest.json";   // 热更新文件信息
    private string appName = "mmorpg.apk";       // app文件
    private Dictionary<string, bool> downErrs = new Dictionary<string, bool>();
    private int downCount = 0;
    private int hotUpdateStatus = 0;
    private ulong downloadBytes = 0;
    private ulong curDownloadBytes = 0;

    public static int HOT_SUCCESS = 1;
    public static int NEW_APK = 2;
    public static int VERSION_EQUAL = 3;
    public static int HAVE_NEW_APK = 4;
    public static int DOWN_ERROR = 5;
    public static int NET_ERROR = 6;

    protected override void Init()
    {
        localPath = Application.persistentDataPath;
        streamAssetPath = Application.streamingAssetsPath;
        localUpdPath = localPath + "/Upd";
        streamAssetAssetsPath = streamAssetPath + "/Assets";
        LoadManifest();
    }

    public UnityEngine.Object LoadPrefabs(string path)
    {
#if UNITY_IOS || UNITY_ANDROID || UNITY_IPHONE 
        return LoadAsset(path);
#else
        return LoadFromResource(path);
#endif
    }

    public byte[] loadBytesBuffer(string filepath)
    {
#if UNITY_IOS || UNITY_ANDROID || UNITY_IPHONE
        var textAsset = ResourceManager.Instance.LoadAsset(filepath) as TextAsset;
        return textAsset.bytes;
#else
        var byteData = GameUtils.ReadFileAsBytes(Path.Combine(Application.dataPath, filepath));
        return byteData;
#endif
    }

    public void LoadPrefabsAnsc(string path, Action<UnityEngine.Object> finish)
    {
#if UNITY_IOS || UNITY_ANDROID || UNITY_IPHONE
        StartCoroutine(LoadAssetAsync(path, finish));
#else
        StartCoroutine(LoadFromResourceAsync(path, finish));
#endif
    }

    public SpriteAtlas LoadSptiteAtlas(string path)
    {
        SpriteAtlas altas = LoadPrefabs(path) as SpriteAtlas;
        return altas;
    }

    public UnityEngine.Object LoadAsset(string path)
    {
        var pathList = path.Split('/');
        var len = pathList.Length;
        if (len < 2)
        {
            return null;
        }
        var assetBundleName = pathList[len - 2];
        assetBundleName = assetBundleName.ToLower();
        var assetName = pathList[len - 1];
        return LoadAssetWithDependencies(assetBundleName, assetName);
    }

    private UnityEngine.Object LoadAssetWithDependencies(string assetBundleName, string assetName)
    {
        AssetBundle asset = null;
        LoadAssetWithDependenciesEx(assetBundleName);
        bundles.TryGetValue(assetBundleName, out asset);
        if (asset != null)
        {
            var assetObj = asset.LoadAsset(assetName);
            return assetObj;
        }
        else
        {
            return null;
        }
    }

    public void LoadAssetWithDependenciesEx(string assetBundleName)
    {
        if (assetBundleManifest == null)
        {
            return;
        }
        AssetBundle asset = null;
        if (!bundles.TryGetValue(assetBundleName, out asset))
        {
            //print(assetBundleName);
            var dependencies = assetBundleManifest.GetAllDependencies(assetBundleName);
            for (int i = 0; i < dependencies.Length; ++i)
            {
                LoadAssetWithDependenciesEx(dependencies[i]);
            }
            var assetBundleFullPath = localUpdPath + "/" + assetBundleName;
            if(File.Exists(assetBundleFullPath))
            {
                //UnityEngine.Debug.Log("load frome upd");
                asset = LoadFromLocalUpd(assetBundleName);
            }
            else
            {
                //UnityEngine.Debug.Log("load frome stream");
                asset = LoadFromStreamAsset(assetBundleName);
            }
            if (asset != null)
            {
                bundles[assetBundleName] = asset;
            }
        }
    }

    public IEnumerator LoadAssetWithDependenciesExAnsc(string assetBundleName)
    {
        if (assetBundleManifest == null)
        {
            yield return null;
        }
        else
        {
            bool cacheFlag = false;
            bundleCaches.TryGetValue(assetBundleName, out cacheFlag);
            while (cacheFlag)
            {
                bundleCaches.TryGetValue(assetBundleName, out cacheFlag);
                yield return null;
            }
            AssetBundle asset = null;
            bundleCaches[assetBundleName] = true;
            if (!bundles.TryGetValue(assetBundleName, out asset))
            {
                //print(assetBundleName);
                var dependencies = assetBundleManifest.GetAllDependencies(assetBundleName);
                for (int i = 0; i < dependencies.Length; ++i)
                {
                    yield return LoadAssetWithDependenciesExAnsc(dependencies[i]);
                }
                var assetBundleFullPath = localUpdPath + "/" + assetBundleName;
                AssetBundleCreateRequest req = null;
                if (File.Exists(assetBundleFullPath))
                {
                    req = LoadFromLocalUpdAsync(assetBundleName);
                }
                else
                {
                    req = LoadFromStreamAssetAsync(assetBundleName);  
                }
                yield return req;
                asset = req.assetBundle;
                if (asset != null)
                {
                    bundles[assetBundleName] = asset;
                }
            }
            bundleCaches[assetBundleName] = false;
        }
    }

    public AssetBundle LoadFromLocalUpd(string assetBundleName)
    {
        var assetFullPath = localUpdPath + "/" + assetBundleName;
        return LoadFromPath(assetFullPath);
    }

    public AssetBundle LoadFromStreamAsset(string assetBundleName)
    {
        var assetFullPath = streamAssetAssetsPath + "/" + assetBundleName;
        return LoadFromPath(assetFullPath);
    }

    public AssetBundle LoadFromPath(string path)
    {
        var asset = AssetBundle.LoadFromFile(path);
        return asset;
    }

    public AssetBundleCreateRequest LoadFromStreamAssetAsync(string assetBundleName)
    {
        var assetFullPath = streamAssetAssetsPath + "/" + assetBundleName;
        return AssetBundle.LoadFromFileAsync(assetFullPath);
    }

    public AssetBundleCreateRequest LoadFromLocalUpdAsync(string assetBundleName)
    {
        var assetFullPath = localUpdPath + "/" + assetBundleName;
        return AssetBundle.LoadFromFileAsync(assetFullPath);
    }

    public UnityEngine.Object LoadFromResource(string path)
    {
        return Resources.Load(path);
    }

    public IEnumerator LoadFromResourceAsync(string path, Action<UnityEngine.Object> finish)
    {
        ResourceRequest req = LoadFromResourceAsyncEx(path);
        yield return req;
        finish(req.asset);
    }

    public ResourceRequest LoadFromResourceAsyncEx(string path)
    {
        return Resources.LoadAsync(path);
    }

    public IEnumerator LoadAssetAsync(string path, Action<UnityEngine.Object> finish)
    {
        var pathList = path.Split('/');
        var len = pathList.Length;
        if (len < 2)
        {
            yield return null;
            finish(null);
        }
        else
        {
            var assetBundleName = pathList[len - 2];
            assetBundleName = assetBundleName.ToLower();
            yield return LoadAssetWithDependenciesExAnsc(assetBundleName);
            AssetBundle asset = null;
            var assetName = pathList[len - 1];
            bundles.TryGetValue(assetBundleName, out asset);
            if (asset != null)
            {
                var assetObj = asset.LoadAsset(assetName);
                finish(assetObj);
            }
            else
            {
                finish(null);
            }
        }
    }

    private void LoadManifest()
    {
        AssetBundle mainAssetBundle = null;
        if (File.Exists(localUpdPath + "/AssetBundles"))
        {
            mainAssetBundle = AssetBundle.LoadFromFile(localUpdPath + "/AssetBundles");
        }
        else
        {
            mainAssetBundle = AssetBundle.LoadFromFile(streamAssetAssetsPath + "/AssetBundles");
        }
        assetBundleManifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
        mainAssetBundle.Unload(false);
    }

    public void CheckUpdate(Action<int> finishCallFunc = null, Action<ulong, ulong> onProgressCallFunc = null)
    {
        StartCoroutine(HotUpdateAsset(finishCallFunc, onProgressCallFunc));
    }

    //从服务器获取版本列表flist
    //检查update的md5值是否有更新，若有则下载update.bin重新载入，并退出main，退出前注意清除对某些的引用。再次重新进入 。
    //检查framework的md5值是否有更新，若有则下载framework.bin，并提示用户重新启动。
    //读取apk安装目录的版本列表文件flist
    //比对服务器版本列表与apk版本里中的大版本号，若不一致则提示用户去APPStore下载。
    //读取upd目录的版本列表文件flist，若存在或flist中存放的core与安装目录列表不一致，表示用户安装了新版本，则清除整个upd目录，并将apk安装目录的flist内容写入upd目录。
    //比对服务器列表与apk列表中version，若版本相同则认为数据无需更新，若版本不同则与服务器的flist对行md5比对，得到需要更新的文件。
    //遍历需要更新的文件列表，若upd存在则校验其md5值，若md5值与服务器相同，则从待更新列表中移除，其目的是为了应对上一次更新过程中，玩家中途退出的情况。
    //逐个更新文件，每个文件更新完毕后，再次校验其md5值，若md5码校验失败，则重新下载此文件。
    //待所有文件更新完毕，重写upd文件中的flist，最后进入游戏
    private IEnumerator HotUpdateAsset(Action<int> finishCallFunc = null, Action<ulong, ulong> onProgressCallFunc = null)
    {
        hotUpdateStatus = HOT_SUCCESS;
        var web = new UnityWebRequest(resUrl + "/" + manifest);
        web.downloadHandler = new DownloadHandlerBuffer();
        yield return web.SendWebRequest();
        if (web.error == null && web.isDone)
        {
            var jsonData = (web.downloadHandler as DownloadHandlerBuffer).text;
            var serModule = JsonUtility.FromJson<JsonModle>(jsonData);
            var locModule = GetLocalApkFlist();
            if (CheckCoreVersion(serModule, locModule))
            {
                var locUpdModule = GetLocalUpdFlist();
                if (CheckLocApkAndLocUpdCoreVersion(locModule, locUpdModule))
                {
                    yield return CheckDownLoadUpdAsset(serModule, locModule, locUpdModule,onProgressCallFunc);
                }
                else
                {
                    hotUpdateStatus = NEW_APK;
                    UnityEngine.Debug.Log("new apk just go!");
                    yield return null;
                }
            }
            else
            {
                hotUpdateStatus = HAVE_NEW_APK;
                UnityEngine.Debug.Log("core version not equal! go to download apk");
                yield return null;
            }
        }
        else
        {
            hotUpdateStatus = NET_ERROR;
            UnityEngine.Debug.Log(web.error);
            yield return null;
        }
        if (hotUpdateStatus <= VERSION_EQUAL)  // 暂时先放这里
        {
            LoadManifest();
        }
        if (finishCallFunc != null)
        {
            finishCallFunc(hotUpdateStatus);
        }
    }

    private JsonModle GetLocalApkFlist()
    {
        var localManifestPath = streamAssetPath + "/" + manifest;
        if (!File.Exists(localManifestPath))
        {
            UnityEngine.Debug.LogError(localManifestPath + " not found!");
        }
        var jsonData = File.ReadAllText(localManifestPath);
        var manifestModule = JsonUtility.FromJson<JsonModle>(jsonData);
        return manifestModule;
    }

    private bool CheckCoreVersion(JsonModle serModuel, JsonModle locModuel)
    {
        if (serModuel.core != locModuel.core)
        {
            Application.OpenURL(resUrl + "/" + appName);
            return false;
        }
        return true;
    }

    private JsonModle GetLocalUpdFlist()
    {
        var localManifestPath = localPath + "/" + manifest;
        if (File.Exists(localManifestPath))
        {
            var jsonData = File.ReadAllText(localManifestPath);
            var manifestModule = JsonUtility.FromJson<JsonModle>(jsonData);
            return manifestModule;
        }
        return null;
    }

    private bool CheckLocApkAndLocUpdCoreVersion(JsonModle locApkModule, JsonModle locUpdModule)
    {
        if (locUpdModule == null || locApkModule.core != locUpdModule.core)
        {
            if (Directory.Exists(localUpdPath))
            {
                Directory.Delete(localUpdPath, true);
            }
            Directory.CreateDirectory(localUpdPath);
            DirectoryInfo folder = new DirectoryInfo(streamAssetAssetsPath);
            var fileList = folder.GetFiles();
            for (int i = 0; i < fileList.Length; ++i)
            {
                var file = fileList[i];
                File.Copy(file.FullName, localUpdPath + "/" + file.Name, true);
            }
            File.Copy(streamAssetPath + "/" + manifest, localPath + "/" + manifest);
            return false;
        }
        return true;
    }

    private IEnumerator CheckDownLoadUpdAsset(JsonModle serModule, JsonModle locModule, JsonModle locUpdModule, Action<ulong, ulong> onProgress)
    {
        if (serModule.version == locModule.version)
        {
            hotUpdateStatus = VERSION_EQUAL;
            UnityEngine.Debug.Log("version is equal just go");
            yield return null;
        }
        else
        {
            downloadBytes = 0;
            Dictionary<string, ulong> sizeMaps = null;
            var updMd5Maps = MapLocUpdMd5(locUpdModule);
            var serMd5Maps = MapServerMd5(serModule, out sizeMaps);
            Dictionary<string, string> updList = new Dictionary<string, string>();
            foreach (var e in serMd5Maps)
            {
                if (updMd5Maps.ContainsKey(e.Key))
                {
                    var md5 = updMd5Maps[e.Key];
                    if (md5 != e.Value)
                    {
                        AddDownBytes(sizeMaps[e.Key]);
                        updList.Add(e.Key, e.Value);
                    }
                }
                else
                {
                    AddDownBytes(sizeMaps[e.Key]);
                    updList.Add(e.Key, e.Value);
                }
            }
            downErrs.Clear();
            downCount = 0;
            curDownloadBytes = 0;
            yield return DownloadUpdList(updList, onProgress);
            updateLocalManifest(ref serModule, ref serMd5Maps);
        }
    }

    private void AddDownBytes(ulong value)
    {
        downloadBytes += value;
    }

    private IEnumerator DownloadUpdList(Dictionary<string, string> updList,Action<ulong,ulong> onProgress)
    {
        if (downCount > 3)
        {
            hotUpdateStatus = DOWN_ERROR;
            yield return null;
        }
        else
        {
            ++downCount;
            Dictionary<string, string> errList = new Dictionary<string, string>();
            foreach (var e in updList)
            {
                var assetBundleName = e.Key;
                var web = new UnityWebRequest(serverAssetsUrl + "/" + assetBundleName);
                web.downloadHandler = new DownloadHandlerFile(localUpdPath + "/" + assetBundleName);
                UnityEngine.Debug.Log("Start Download File " + assetBundleName);
                web.SendWebRequest();
                while(!web.isDone)
                {
                    onProgress(curDownloadBytes + web.downloadedBytes, downloadBytes);
                    yield return null;
                }
                if (web.error == null && web.isDone)
                {
                    var curMd5 = GetMD5HashFromFile(localUpdPath + "/" + assetBundleName);
                    if (curMd5 != e.Value)
                    {
                        errList.Add(assetBundleName, e.Value);
                        downErrs[assetBundleName] = true;
                    }
                    else
                    {
                        curDownloadBytes += web.downloadedBytes;
                        onProgress(curDownloadBytes, downloadBytes);
                        downErrs[assetBundleName] = false;
                    }
                    UnityEngine.Debug.Log("End Download File " + assetBundleName);
                }
                else
                {
                    downErrs[assetBundleName] = true;
                    errList.Add(assetBundleName, e.Value);
                    UnityEngine.Debug.Log(web.error);
                }
            }
            if (errList.Count > 0)
            {
                yield return DownloadUpdList(errList, onProgress);
            }
        }
    }

    static string GetMD5HashFromFile(string fileName)
    {
        try
        {
            FileStream file = new FileStream(fileName, FileMode.Open);
            System.Security.Cryptography.MD5 md5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
            byte[] retVal = md5.ComputeHash(file);
            file.Close();
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < retVal.Length; i++)
            {
                sb.Append(retVal[i].ToString("x2"));
            }
            return sb.ToString();
        }
        catch (Exception ex)
        {
            throw new Exception("GetMD5HashFromFile() fail,error:" + ex.Message);
        }
    }

    private Dictionary<string, string> MapLocUpdMd5(JsonModle locUpdModule)
    {
        Dictionary<string, string> result = new Dictionary<string, string>();
        DirectoryInfo folder = new DirectoryInfo(localUpdPath);
        var fileList = folder.GetFiles();
        var len1 = fileList.Length;
        for (int i = 0; i < len1; ++i)
        {
            var file = fileList[i];
            result[file.Name] = "";
        }
        var len2 = locUpdModule.fileList.Length;
        for (int i = 0; i < len2; ++i)
        {
            var fileV = locUpdModule.fileList[i];
            var list = fileV.Split(':');
            if (list.Length >= 2)
            {
                if (result.ContainsKey(list[0]))
                {
                    result[list[0]] = list[1];
                }
            }
        }
        return result;
    }

    private Dictionary<string, string> MapServerMd5(JsonModle module,out Dictionary<string,ulong> sizeMaps)
    {
        sizeMaps = new Dictionary<string, ulong>();
        var md5Maps = new Dictionary<string, string>();
        var fileList = module.fileList;
        var len = fileList.Length;
        for (int i = 0; i < len; ++i)
        {
            var file = fileList[i].Split(':');
            if (file.Length >= 2)
            {
                md5Maps[file[0]] = file[1];
            }
            if (file.Length > 2)
            {
                sizeMaps[file[0]] = ulong.Parse(file[2]);
            }
        }
        return md5Maps;
    }

    //public void CheckDownLoadAsset(Action finish)
    //{
    //    StartCoroutine(DownLoadAsset(finish));
    //}

    //private IEnumerator DownLoadAsset(Action finish)
    //{
    //    var localManiFest = persistentDataPath + "/" + manifest;
    //    if (!File.Exists(localManiFest))
    //    {
    //        var web = new UnityWebRequest(resUrl + "/" + manifest);
    //        web.downloadHandler = new DownloadHandlerFile(localManiFest);
    //        yield return web.SendWebRequest();
    //        if (web.error == null && web.isDone)
    //        {
    //            var jsonData = File.ReadAllText(localManiFest);
    //            yield return DownloadAllFile(localManiFest, jsonData);
    //        }
    //        else
    //        {
    //            File.Delete(localManiFest);
    //            print(web.error);
    //        }
    //    }
    //    else
    //    {
    //        print("存在");
    //        yield return DownloadDiffAsset(localManiFest);
    //    }
    //    yield return LoadManifest();
    //    finish();
    //}

    //private IEnumerator DownloadDiffAsset(string localManiFest)
    //{
    //    var web = new UnityWebRequest(resUrl + "/" + manifest);
    //    web.downloadHandler = new DownloadHandlerBuffer();
    //    yield return web.SendWebRequest();
    //    if (web.error == null && web.isDone)
    //    {
    //        var downData = (web.downloadHandler as DownloadHandlerBuffer).text;
    //        var downModule = JsonUtility.FromJson<JsonModle>(downData);

    //        var localData = File.ReadAllText(localManiFest);
    //        var localModule = JsonUtility.FromJson<JsonModle>(localData);
    //        if (localModule.core != downModule.core)  // 版本不一致下载Apk
    //        {
    //            Application.OpenURL(resUrl + "/mmorpg.apk");
    //            File.Delete(localManiFest);
    //            Application.Quit();
    //            //print("大版本");
    //            //File.WriteAllText(localManiFest, downData);
    //            //yield return DownloadAllFile(localManiFest, downData);
    //        }
    //        else if (localModule.version != downModule.version) // 版本不同比较每个文件md5
    //        {
    //            print("小版本");
    //            var downMd5Maps = MapMd5(downModule);
    //            var localMd5Maps = MapMd5(localModule);
    //            List<string> needDownList = new List<string>();
    //            foreach (var e in localMd5Maps)  // 下载差异文件
    //            {
    //                string downMd5 = null;
    //                if (downMd5Maps.TryGetValue(e.Key, out downMd5))
    //                {
    //                    if (e.Value != downMd5)
    //                    {
    //                        needDownList.Add(e.Key);
    //                    }
    //                }
    //            }
    //            foreach (var e in downMd5Maps)   // 本地不存在直接下载
    //            {
    //                string value = "";
    //                if (!localMd5Maps.TryGetValue(e.Key, out value))
    //                {
    //                    needDownList.Add(e.Key);
    //                }
    //                localMd5Maps[e.Key] = e.Value;
    //            }
    //            foreach (var e in needDownList)
    //            {
    //                yield return DownloadSignalFile(e);
    //            }
    //            updateLocalManifest(ref localModule, ref localMd5Maps);
    //        }
    //    }
    //    else
    //    {
    //        print(web.error);
    //    }
    //}

    private void updateLocalManifest(ref JsonModle newModule, ref Dictionary<string, string> maps)
    {
        newModule.fileList = new string[maps.Count];
        var index = 0;
        foreach (var e in maps)
        {
            bool flag = false;
            downErrs.TryGetValue(e.Key, out flag);
            if (!flag)
            {
                newModule.fileList[index] = e.Key + ":" + e.Value;
                index = index + 1;
            }
        }
        var localManiFest = localPath + "/" + manifest;
        var jsonData = JsonUtility.ToJson(newModule);
        //print(jsonData);
        File.WriteAllText(localManiFest, jsonData);
    }


    //private IEnumerator DownloadAllFile(string path,string jsonData)
    //{
    //    var module = JsonUtility.FromJson<JsonModle>(jsonData);
    //    var fileList = module.fileList;
    //    var len = fileList.Length;
    //    for (int i = 0; i < len; ++i)
    //    {
    //        var file = fileList[i].Split(':');
    //        if (file.Length >= 2)
    //        {
    //            var assetBundleName = file[0];
    //            print(assetBundleName);
    //            yield return DownloadSignalFile(assetBundleName);
    //        }
    //    }
    //    yield return null;
    //}

    //private IEnumerator DownloadSignalFile(string assetBundleName)
    //{
    //    var web = new UnityWebRequest(resUrl + "/Assets/" + assetBundleName);
    //    web.downloadHandler = new DownloadHandlerFile(localPath + "/" + assetBundleName);
    //    print("Start Download File " + assetBundleName);
    //    yield return web.SendWebRequest();
    //    print("End Download File " + assetBundleName);
    //}

    public void UnloadAsset()
    {
        foreach (var e in bundles)
        {
            e.Value.Unload(true);
        }
        bundles.Clear();
    }
}
